namespace ScrumSprintMonitor.Modules.WorkItemMonitor.ViewModels
{
	#region #using Directives

	using System;
	using System.Collections.Generic;
	using System.Collections.ObjectModel;
	using System.ComponentModel;
	using System.Diagnostics;
	using System.Linq;
	using System.Threading;
	using System.Windows.Media;
	using System.Windows.Threading;

	using Infrastructure;
	using Infrastructure.Configuration;

	using Microsoft.Practices.ServiceLocation;
	using Microsoft.Practices.Unity;

	using UI.Wpf.Converters;
	using UI.Wpf.ViewModels;

	#endregion

	public class TeamMembersListViewModel : SprintViewModelBase,
	                                        ITeamMembersListViewModel
	{
		static TeamMembersListViewModel()
		{
			var dependsOn = Register<TeamMembersListViewModel>();

			dependsOn.Call(me => me.OnPropertyChanged("HasException")).OnChanged(me => me.Exception);
			dependsOn.Call(me => me.CheckCurrentSprintInitialized()).OnChanged(me => me.CurrentSprint.LastUpdate);
			dependsOn.Call(me => me.AvailableHours = (me.CurrentSprint != null ? me.CurrentSprint.AvailableTime.TotalHours : double.NaN)).OnChanged(
			                                                                                                                                        me =>
			                                                                                                                                        me.
			                                                                                                                                        	ReportableData
			                                                                                                                                        	.
			                                                                                                                                        	CurrentSprint
			                                                                                                                                        	.
			                                                                                                                                        	AvailableTime);
			dependsOn.Call(me => me.AttachToCurrentSprintTeamMembers()).OnChanged(me => me.ReportableData.CurrentSprint.TeamMembers);
			dependsOn.Call(me => me.AttachToCurrentSprintBurndownDataPoints()).OnChanged(me => me.ReportableData.CurrentSprint.Burndown.DataPoints);

			dependsOn.Call(me => me.OnPropertyChanged("SprintWarningBackgroundColor")).OnChanged(me => me.AvailableHours).OnChanged(
			                                                                                                                        me =>
			                                                                                                                        me.NotDoneWork_TotalHours).
				OnChanged(me => me.ActiveWork_TotalHoursRemaining);
		}

		public TeamMembersListViewModel(IServiceLocator serviceLocator, IUnityContainer container, IRemainingWorkStatusEvaluator remainingWorkStatusEvaluator,
		                                IPresentationConfiguration presentationConfiguration)
			: base(serviceLocator, true)
		{
			if (SyncContext != null)
			{
				this.timer = new Timer(OnTimer, null, Timeout.Infinite, Timeout.Infinite);
			}

            this.container = container;
			this.remainingWorkStatusEvaluator = remainingWorkStatusEvaluator;
			this.presentationConfiguration = presentationConfiguration;
		}

		#region Private Fields

		private readonly Comparer comparer = new Comparer();
        private readonly IUnityContainer container;
		private readonly IPresentationConfiguration presentationConfiguration;
		private readonly IRemainingWorkStatusEvaluator remainingWorkStatusEvaluator;
		private readonly object sortedTeamMembersListLockObject = new object();
		private readonly Timer timer;
		private readonly Dictionary<ITeamMember, ITeamMemberViewModel> viewModelDictionary = new Dictionary<ITeamMember, ITeamMemberViewModel>();

		private double activeWork_TotalHoursRemaining;
		private double availableHours;
		private BindingList<BurndownDataPoint> currentSprintBurndownDataPoints;

		/// <summary>
		///   Reference to CurrentSprint.TeamMembers.
		/// </summary>
		private BindingList<ITeamMember> currentSprintTeamMembers;

		/// <summary>
		///   Local copy of the list in CurrentSprint.TeamMembers.
		/// </summary>
		private List<ITeamMember> currentSprintTeamMembersCopy;

		private bool initialized;
		private double maximumTeamMemberBurndownChartHours;

		private double notDoneWork_TotalHours;
		private int numberOfColumnsInGridView;

		/// <summary>
		///   Sorted bindable list of UnassignedWork + CurrentSprint.TeamMembers.
		/// </summary>
		private ObservableCollection<ITeamMemberViewModel> teamMembersIncludingUnassigned;

		#endregion

		#region Private Methods

		private double AvailableHours
		{
			get
			{
				GuardDisposed();

				return this.availableHours;
			}
			set
			{
				GuardDisposed();

				if (value != this.availableHours)
				{
					this.availableHours = value;

					OnPropertyChanged("AvailableHours");
				}
			}
		}

		private double NotDoneWork_TotalHours
		{
			get
			{
				GuardDisposed();

				return this.notDoneWork_TotalHours;
			}
			set
			{
				GuardDisposed();

				if (value != this.notDoneWork_TotalHours)
				{
					this.notDoneWork_TotalHours = value;

					OnPropertyChanged("NotDoneWork_TotalHours");
				}
			}
		}

		private double ActiveWork_TotalHoursRemaining
		{
			get
			{
				GuardDisposed();

				return this.activeWork_TotalHoursRemaining;
			}
			set
			{
				GuardDisposed();

				if (value != this.activeWork_TotalHoursRemaining)
				{
					this.activeWork_TotalHoursRemaining = value;

					OnPropertyChanged("ActiveWork_TotalHoursRemaining");
				}
			}
		}

		private void RecalculateMaximumTeamMemberBurndownChartHours()
		{
			var teamMembers = TeamMembersIncludingUnassigned.Where(t => !t.Unassigned);
			double max = (teamMembers.Any() ? teamMembers.Max(t => GetMaximumTotalHoursForTeamMember(t)) : 0.0);

			MaximumTeamMemberBurndownChartHours = Math.Max(1.0, max);
		}

		private static double GetMaximumTotalHoursForTeamMember(ITeamMemberViewModel t)
		{
			return (t.BurndownDataPoints.Any() ? t.BurndownDataPoints.Max(x => x.TotalWork) : 0.0);
		}

		private void RecalculateMaximumTeamMemberBurndownChartHours(ITeamMemberViewModel newTeamMemberViewModel)
		{
			if (newTeamMemberViewModel.Unassigned || !newTeamMemberViewModel.BurndownDataPoints.Any())
			{
				return;
			}

			MaximumTeamMemberBurndownChartHours = Math.Max(MaximumTeamMemberBurndownChartHours,
			                                               Math.Max(1.0, GetMaximumTotalHoursForTeamMember(newTeamMemberViewModel)));
		}

		private void FillTeamMembersIncludingUnassignedList(ICollection<ITeamMember> teamMembers)
		{
			lock (this.sortedTeamMembersListLockObject)
			{
				var excludedTeamMembers = TeamMembersIncludingUnassigned.Where(t => !teamMembers.Contains(t.Model));

				if (excludedTeamMembers.Any())
				{
					excludedTeamMembers = new List<ITeamMemberViewModel>(excludedTeamMembers);
					foreach (var excludedTeamMember in excludedTeamMembers)
					{
						RemoveTeamMemberViewModel(excludedTeamMember, true, true);
					}
				}

				if (teamMembers != null)
				{
					foreach (var teamMember in teamMembers)
					{
						AddTeamMember(teamMember, false, false);
					}
				}
			}

			VerifyNumberOfColumnsInGridViewChanged();
		}

		private void FillTeamMembersIncludingUnassignedList()
		{
			FillTeamMembersIncludingUnassignedList(this.currentSprintTeamMembers);
		}

		private void VerifyNumberOfColumnsInGridViewChanged()
		{
			int currentNumberOfColumnsInGridView;

			lock (this.sortedTeamMembersListLockObject)
			{
				currentNumberOfColumnsInGridView = Math.Max(1, (int) Math.Ceiling(Math.Sqrt(TeamMembersIncludingUnassigned.Count)));
			}

			NumberOfColumnsInGridView = currentNumberOfColumnsInGridView;
		}

		private void CheckCurrentSprintInitialized()
		{
			if (CurrentSprint != null)
			{
				this.initialized = true;
			}
		}

		private void AddTeamMember(ITeamMember newTeamMember, bool showOnly, bool markUpdated)
		{
			if (newTeamMember == null)
			{
				throw new ArgumentNullException("newTeamMember");
			}

			if (IsTeamMemberVisible(newTeamMember))
			{
				bool added = false;

				lock (this.sortedTeamMembersListLockObject)
				{
					var newTeamMemberViewModel = GetViewModel(newTeamMember);

					if (!TeamMembersIncludingUnassigned.Contains(newTeamMemberViewModel))
					{
						int index = FindIndexForTeamMember(TeamMembersIncludingUnassigned, newTeamMemberViewModel);

						TeamMembersIncludingUnassigned.Insert(index, newTeamMemberViewModel);

						OnTeamMemberViewModelAdded(newTeamMemberViewModel, showOnly, markUpdated);

						added = true;
					}
				}

				if (added)
				{
					VerifyNumberOfColumnsInGridViewChanged();
				}
			}
		}

		private void RemoveTeamMember(ITeamMember removedTeamMember)
		{
			RemoveTeamMember(removedTeamMember, true);
		}

		private void RemoveTeamMember(ITeamMember removedTeamMember, bool permanent)
		{
			if (removedTeamMember == null)
			{
				throw new ArgumentNullException("removedTeamMember");
			}

			lock (this.sortedTeamMembersListLockObject)
			{
				var teamMemberViewModel = GetViewModel(removedTeamMember, false);

				if (teamMemberViewModel == null)
				{
					return;
				}

				RemoveTeamMemberViewModel(teamMemberViewModel, permanent, false);
			}
		}

		private void RemoveTeamMemberViewModel(ITeamMemberViewModel removedTeamMemberViewModel, bool permanent, bool inBatchOperation)
		{
			if (removedTeamMemberViewModel == null)
			{
				throw new ArgumentNullException("removedTeamMemberViewModel");
			}

			bool removed = false;

			lock (this.sortedTeamMembersListLockObject)
			{
				if (TeamMembersIncludingUnassigned.Remove(removedTeamMemberViewModel))
				{
					OnTeamMemberViewModelRemoved(removedTeamMemberViewModel, permanent);

					removed = true;
				}
			}

			if (removed && !inBatchOperation)
			{
				PostAction(VerifyNumberOfColumnsInGridViewChanged);
			}
		}

		private void ShowTeamMember(ITeamMember teamMember)
		{
			AddTeamMember(teamMember, true, false);
		}

		private void HideTeamMember(ITeamMember removedTeamMember)
		{
			RemoveTeamMember(removedTeamMember, false);
		}

		private void OnTeamMemberViewModelAdded(ITeamMemberViewModel newTeamMemberViewModel, bool showingOnly, bool markUpdated)
		{
			if (!showingOnly)
			{
				newTeamMemberViewModel.PropertyChanged += OnTeamMemberViewModelPropertyChanged;
			}

			RecalculateMaximumTeamMemberBurndownChartHours(newTeamMemberViewModel);

			if (markUpdated)
			{
				newTeamMemberViewModel.LastUpdate = SystemTime.Now();
			}
		}

		private void OnTeamMemberViewModelRemoved(ITeamMemberViewModel removedTeamMemberViewModel, bool permanent)
		{
			if (permanent)
			{
				removedTeamMemberViewModel.PropertyChanged -= OnTeamMemberViewModelPropertyChanged;
			}

			if (!removedTeamMemberViewModel.Unassigned)
			{
				RecalculateMaximumTeamMemberBurndownChartHours();
			}
		}

		private void ApplyFilterToTeamMember(ITeamMemberViewModel changedTeamMemberViewModel)
		{
			lock (this.sortedTeamMembersListLockObject)
			{
				bool exists = TeamMembersIncludingUnassigned.Contains(changedTeamMemberViewModel);
				var teamMember = changedTeamMemberViewModel.Model;

				if (IsTeamMemberVisible(teamMember))
				{
					if (!exists)
					{
						ShowTeamMember(teamMember);
					}
				}
				else
				{
					if (exists)
					{
						HideTeamMember(teamMember);
					}
				}
			}
		}

		private void ReSortTeamMember(ITeamMemberViewModel changedTeamMemberViewModel)
		{
			lock (this.sortedTeamMembersListLockObject)
			{
				int currentIndex = TeamMembersIncludingUnassigned.IndexOf(changedTeamMemberViewModel);

				if (currentIndex >= 0)
				{
					int newIndex = FindIndexForTeamMember(TeamMembersIncludingUnassigned, changedTeamMemberViewModel);
					if (newIndex != currentIndex && newIndex != currentIndex + 1)
					{
						if (newIndex > currentIndex)
						{
							--newIndex;
						}

						TeamMembersIncludingUnassigned.Move(currentIndex, newIndex);
					}
				}
			}
		}

		private int FindIndexForTeamMember(IEnumerable<ITeamMemberViewModel> teamMembersList, ITeamMemberViewModel newTeamMemberViewModel)
		{
			int index = 0;

			foreach (var teamMemberViewModel in teamMembersList)
			{
				if (teamMemberViewModel != newTeamMemberViewModel)
				{
					if (this.comparer.Compare(newTeamMemberViewModel, teamMemberViewModel) <= 0)
					{
						break;
					}
				}

				++index;
			}

			return index;
		}

		private void RefreshRecentlyUpdatedFlag()
		{
			lock (this.sortedTeamMembersListLockObject)
			{
				foreach (var teamMemberViewModel in TeamMembersIncludingUnassigned)
				{
					teamMemberViewModel.RefreshRecentlyUpdatedFlag();
				}
			}
		}

	    private ITeamMemberViewModel GetViewModel(ITeamMember teamMember, bool createIfNotExists = true)
		{
			ITeamMemberViewModel teamMemberViewModel = null;

			if (teamMember != null && !this.viewModelDictionary.TryGetValue(teamMember, out teamMemberViewModel) && createIfNotExists)
			{
                var parameters = new[]
				                 	{
				                 		new ParameterOverride("model", teamMember),
				                 		new ParameterOverride("currentSprint", CurrentSprint),
				                 		new ParameterOverride("teamMembersListViewModel", this)
				                 	};
                teamMemberViewModel = this.container.Resolve<ITeamMemberViewModel>(parameters);

				this.viewModelDictionary.Add(teamMember, teamMemberViewModel);
			}

			return teamMemberViewModel;
		}

		private void AttachToCurrentSprintTeamMembers()
		{
			this.initialized = false;

			if (CurrentSprint == null)
			{
				DetachFromCurrentSprintTeamMembers();
			}
			else if (CurrentSprint.TeamMembers != this.currentSprintTeamMembers)
			{
				DetachFromCurrentSprintTeamMembers();

				this.currentSprintTeamMembers = CurrentSprint.TeamMembers;
				this.currentSprintTeamMembersCopy = new List<ITeamMember>(this.currentSprintTeamMembers);
				if (this.currentSprintTeamMembers != null)
				{
					FillTeamMembersIncludingUnassignedList();

					this.currentSprintTeamMembers.ListChanged += OnCurrentSprintTeamMembersListChanged;
				}

				OnPropertyChanged("TeamMembersIncludingUnassigned");
			}

			RecalculateMaximumTeamMemberBurndownChartHours();
		}

		private void AttachToCurrentSprintBurndownDataPoints()
		{
			DetachCurrentSprintBurndownDataPoints();

			if (CurrentSprint != null && CurrentSprint.Burndown != null)
			{
				this.currentSprintBurndownDataPoints = CurrentSprint.Burndown.DataPoints;
				if (this.currentSprintBurndownDataPoints != null)
				{
					this.currentSprintBurndownDataPoints.ListChanged += OnCurrentSprintBurndownDataPointsListChanged;
				}
			}
		}

		private void DetachCurrentSprintBurndownDataPoints()
		{
			if (this.currentSprintBurndownDataPoints != null)
			{
				this.currentSprintBurndownDataPoints.ListChanged -= OnCurrentSprintBurndownDataPointsListChanged;
				this.currentSprintBurndownDataPoints = null;
			}
		}

		private void DetachFromCurrentSprintTeamMembers()
		{
			if (this.currentSprintTeamMembers != null)
			{
				this.currentSprintTeamMembers.ListChanged -= OnCurrentSprintTeamMembersListChanged;
				this.currentSprintTeamMembers = null;
			}

			this.currentSprintTeamMembersCopy = null;
		}

		private void OnCurrentSprintTeamMembersListChanged(object sender, ListChangedEventArgs e)
		{
			switch (e.ListChangedType)
			{
				case ListChangedType.Reset:
					this.currentSprintTeamMembersCopy.Clear();
					this.currentSprintTeamMembersCopy.AddRange(this.currentSprintTeamMembers);

					PostAction(DispatcherPriority.DataBind, () => FillTeamMembersIncludingUnassignedList(new List<ITeamMember>(this.currentSprintTeamMembers)));
					break;
				case ListChangedType.ItemAdded:
				{
					var newTeamMember = this.currentSprintTeamMembers[e.NewIndex];

					this.currentSprintTeamMembersCopy.Add(newTeamMember);
					bool markUpdated = this.initialized;
					PostAction(DispatcherPriority.DataBind, () => AddTeamMember(newTeamMember, false, markUpdated));
					break;
				}
				case ListChangedType.ItemDeleted:
				{
					var removedTeamMember = this.currentSprintTeamMembersCopy[e.NewIndex];

					this.currentSprintTeamMembersCopy.RemoveAt(e.NewIndex);
					PostAction(DispatcherPriority.DataBind, () => RemoveTeamMember(removedTeamMember));
					break;
				}
				case ListChangedType.ItemMoved:
				{
					var movedTeamMember = this.currentSprintTeamMembersCopy[e.OldIndex];

					this.currentSprintTeamMembersCopy.RemoveAt(e.OldIndex);
					this.currentSprintTeamMembersCopy.Insert(e.NewIndex, movedTeamMember);
					break;
				}
			}
		}

		private void OnTimer(object state)
		{
			PostAction(RefreshRecentlyUpdatedFlag);
		}

		private static bool IsTeamMemberVisible(ITeamMember teamMember)
		{
			return teamMember.Information.DedicatedToSprint ||
			       (teamMember.RemainingWorkload.SprintWork + teamMember.RemainingWorkload.UnassignedSprintWork > 0.0 ||
			        teamMember.RemainingWorkload.WorkItemCount > 0);
		}

		#endregion

		#region Overrides

		protected override void OnSprintDataInitialized()
		{
			GuardDisposed();

			if (this.timer != null)
			{
				this.timer.Change(10000, 10000);
			}
		}

		protected override void OnCalculateWorkItemData(IList<IWorkItemAdapter> workItems)
		{
			GuardDisposed();

			base.OnCalculateWorkItemData(workItems);

			RefreshRecentlyUpdatedFlag();

			ActiveWork_TotalHoursRemaining =
                workItems.Sum(x => x.Type == WorkItemType.SprintBacklogItem && x.State == WorkItemState.InProgress ? x.GetReportableRemainingWork() : 0.0);
            NotDoneWork_TotalHours = workItems.Sum(x => x.Type == WorkItemType.SprintBacklogItem && x.State == WorkItemState.NotDone ? x.GetReportableRemainingWork() : 0.0);
		}

		protected override void Dispose(bool disposing)
		{
			base.Dispose(disposing);

			if (disposing)
			{
				if (this.timer != null)
				{
					this.timer.Dispose();
				}

				DetachCurrentSprintBurndownDataPoints();
				DetachFromCurrentSprintTeamMembers();
			}
		}

		#endregion

		#region ITeamMembersListViewModel Members

		Exception ITeamMembersListViewModel.Exception
		{
			get
			{
				GuardDisposed();

				return Exception;
			}
		}

		public bool HasException
		{
			get { return Exception != null; }
		}

		public ICollection<ITeamMemberViewModel> UnassignedTeamMembers
		{
			get { return TeamMembersIncludingUnassigned.Where(m => m.Unassigned).ToList(); }
		}

		public ObservableCollection<ITeamMemberViewModel> TeamMembersIncludingUnassigned
		{
			get
			{
				GuardDisposed();

				lock (this.sortedTeamMembersListLockObject)
				{
					if (this.teamMembersIncludingUnassigned == null)
					{
						this.teamMembersIncludingUnassigned = new ObservableCollection<ITeamMemberViewModel>();

						AttachToCurrentSprintTeamMembers();
					}
				}
				return this.teamMembersIncludingUnassigned;
			}
		}

		public double MaximumTeamMemberBurndownChartHours
		{
			[DebuggerStepThrough]
			get { return this.maximumTeamMemberBurndownChartHours; }
			private set
			{
				if (value == MaximumTeamMemberBurndownChartHours)
				{
					return;
				}

				this.maximumTeamMemberBurndownChartHours = value;

				OnPropertyChanged("MaximumTeamMemberBurndownChartHours");
			}
		}

		public int NumberOfColumnsInGridView
		{
			[DebuggerStepThrough]
			get
			{
				GuardDisposed();

				return this.numberOfColumnsInGridView;
			}
			set
			{
				if (value == NumberOfColumnsInGridView)
				{
					return;
				}

				this.numberOfColumnsInGridView = value;

				OnPropertyChanged("NumberOfColumnsInGridView");
			}
		}

		public Color SprintWarningBackgroundColor
		{
			get
			{
				GuardDisposed();

				double remainingWork = ActiveWork_TotalHoursRemaining + NotDoneWork_TotalHours;
				double ratio = remainingWork / AvailableHours;

				var status = this.remainingWorkStatusEvaluator.Evaluate(remainingWork, AvailableHours);
				var color = RemainingWorkStatusColorHelper.GetColor(status, ratio, "Background", this.presentationConfiguration);

				return color;
			}
		}

		#endregion

		#region Event Handlers

		private void OnTeamMemberViewModelPropertyChanged(object sender, PropertyChangedEventArgs e)
		{
			switch (e.PropertyName)
			{
				case "RemainingSprintWorkWithUnassigned":
				case "RemainingWorkItemCount":
					PostAction(() => ApplyFilterToTeamMember((ITeamMemberViewModel) sender));
					break;
			}

			switch (e.PropertyName)
			{
				case "RemainingSprintWorkWithUnassigned":
				case "RemainingPotentialSprintHours":
				case "RemainingSprintHours":
					PostAction(() => ReSortTeamMember((ITeamMemberViewModel) sender));
					break;
			}
		}

		private void OnCurrentSprintBurndownDataPointsListChanged(object sender, ListChangedEventArgs e)
		{
			switch (e.ListChangedType)
			{
				case ListChangedType.Reset:
				case ListChangedType.ItemAdded:
					RecalculateMaximumTeamMemberBurndownChartHours();
					break;
			}
		}

		#endregion

		#region Nested type: Comparer

		private class Comparer : IComparer<ITeamMemberViewModel>
		{
			#region IComparer<ITeamMemberViewModel> Members

			public int Compare(ITeamMemberViewModel x, ITeamMemberViewModel y)
			{
				if (x == null)
				{
					return (y == null ? 0 : 1);
				}
				if (y == null)
				{
					return -1;
				}
				if (x.Unassigned)
				{
					if (!y.Unassigned)
					{
						return -1;
					}
				}
				else if (y.Unassigned)
				{
					return 1;
				}

				double xRatio = CalcWorkloadRatio(x);
				double yRatio = CalcWorkloadRatio(y);

				if (double.IsPositiveInfinity(xRatio) && double.IsPositiveInfinity(yRatio))
				{
					return 0;
				}

				if (xRatio == yRatio)
				{
					return -Math.Sign(y.RemainingSprintHours - x.RemainingSprintHours);
				}

				return Math.Sign(yRatio - xRatio);
			}

			#endregion

			private static double CalcWorkloadRatio(ITeamMemberViewModel workload)
			{
				double sprintWorkWithUnassigned = workload.RemainingSprintWorkWithUnassigned;
				if (double.IsNaN(sprintWorkWithUnassigned))
				{
					sprintWorkWithUnassigned = double.PositiveInfinity;
				}

				return sprintWorkWithUnassigned / Math.Max(workload.RemainingPotentialSprintHours, 0.01);
			}
		}

		#endregion
	}
}